﻿/*
 * Copyright (c) 2015-2016 Constantijn Evenhuis (blueprints@const.nl)
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
using Blueprints.Defaults;
using Blueprints.IO;
using Blueprints.Resolution;
using System;
using System.Collections.Generic;
using System.Reflection;

namespace Blueprints
{
    /// <summary>
    /// A context within which blueprints can be deserialized.
    /// </summary>
    /// <typeparam name="ResultType">The base type blueprints are deserialized to.</typeparam>
    public abstract partial class DeserializationContext<ResultType> : IDisposable
    {
        private readonly Dictionary<string, DeserializedNodeInfo<ResultType>> nodes = new Dictionary<string, DeserializedNodeInfo<ResultType>>(StringComparer.Ordinal);

        /// <summary>
        /// Creates a new DeserializationContext.
        /// </summary>
        /// <param name="targetType">The root type of the blueprint object.</param>
        public DeserializationContext(Type targetType)
            : this(GetTypeResolver(targetType), GetBlueprintResolver(targetType))
        {
        }

        /// <summary>
        /// Creates a new DeserializationContext.
        /// </summary>
        /// <param name="typeResolver">The type resolver being used within this context.</param>
        /// <param name="blueprintResolver">The blueprint resolver being used within this context.</param>
        public DeserializationContext(ITypeResolver typeResolver, IBlueprintResolver blueprintResolver)
        {
            TypeResolver = new TypeResolverCache(typeResolver);
            BlueprintResolver = blueprintResolver;
        }

        /// <summary>
        /// Gets the type resolver being used within this context.
        /// </summary>
        public ITypeResolver TypeResolver
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the blueprint resolver being used within this context.
        /// </summary>
        public IBlueprintResolver BlueprintResolver
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the type resolver to use for the <paramref name="targetType"/>.
        /// </summary>
        /// <param name="targetType">The root type of the blueprint object.</param>
        /// <returns>The type resolver to use for the <paramref name="targetType"/>.</returns>
        public static ITypeResolver GetTypeResolver(Type targetType)
        {
            return GetResolver<TypeResolverAttribute, ITypeResolver>(targetType, () => new DefaultTypeResolver(targetType));
        }

        /// <summary>
        /// Gets the blueprint resolver to use for the <paramref name="targetType"/>.
        /// </summary>
        /// <param name="targetType">The root type of the blueprint object.</param>
        /// <returns>The blueprint resolver to use for the <paramref name="targetType"/>.</returns>
        public static IBlueprintResolver GetBlueprintResolver(Type targetType)
        {
            return GetResolver<BlueprintResolverAttribute, IBlueprintResolver>(targetType, () => new DefaultBlueprintResolver());
        }

        /// <summary>
        /// Clears previous deserialization results from the cache for the indicated document.
        /// </summary>
        /// <param name="documentUri">The document for which to clear the results.</param>
        public void ClearResults(Uri documentUri)
        {
            string documentUriString = documentUri.ToString();
            List<string> uris = new List<string>(nodes.Keys);
            foreach (string uri in uris)
            {
                string uriWithoutFragment;
                int index = uri.IndexOf('#');
                if (index >= 0)
                {
                    uriWithoutFragment = uri.Substring(0, index);
                }
                else
                {
                    uriWithoutFragment = uri;
                }

                if (string.Equals(documentUriString, uriWithoutFragment))
                {
                    nodes.Remove(uri);
                }
            }
        }

        /// <summary>
        /// Clears all previous deserialization results from the cache.
        /// </summary>
        public void ClearResults()
        {
            nodes.Clear();
        }

        /// <summary>
        /// Disposes the deserialization context, releasing all used resources.
        /// </summary>
        public virtual void Dispose()
        {
            ClearResults();
        }

        internal ResultType Deserialize(NodeInfoReader reader, Type expectedType)
        {
            ResultType obj;
            NodeInfo nodeInfo = reader.Current;

            // The node may have already been deserialized
            DeserializedNodeInfo<ResultType> earlierResult;
            if (TryGetDeserializedNode(nodeInfo, out earlierResult))
            {
                if (expectedType.GetTypeInfo().IsAssignableFrom(earlierResult.TypeInfo))
                {
                    obj = earlierResult.Object;
                }
                else
                {
                    obj = CreateTypeError(nodeInfo, expectedType, DeserializationResult.TypeNotCompatible, earlierResult.Type);
                }

                reader.MoveToEndNode();
                return obj;
            }

            // If specified, validate the node's type
            Type actualType;
            DeserializationResult result = GetActualType(nodeInfo.Node, expectedType, out actualType);
            if (result != DeserializationResult.Success)
            {
                obj = CreateTypeError(nodeInfo, expectedType, result, actualType);
                reader.MoveToEndNode();
                return obj;
            }

            // If specified, resolve the node's link
            if (!string.IsNullOrEmpty(nodeInfo.Node.Link))
            {
                obj = DeserializeLink(nodeInfo, actualType);
                SetDeserializedNode(nodeInfo, new DeserializedNodeInfo<ResultType>(obj, actualType));
                reader.MoveToEndNode();
                return obj;
            }

            // Now that null is ruled out, deserialize to its underlying type when dealing with Nullable<>s.
            TypeInfo actualTypeInfo = actualType.GetTypeInfo();
            if (actualTypeInfo.IsGenericType
                && actualTypeInfo.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                actualType = Nullable.GetUnderlyingType(actualType);
            }

            TypeCategory typeCategory;
            try
            {
                typeCategory = TypeResolver.GetTypeCategory(actualType);
            }
            catch (BlueprintException bex)
            {
                throw new BlueprintDocumentException(bex, nodeInfo.Path);
            }

            switch (typeCategory)
            {
                case TypeCategory.NotInstantiable:
                    obj = CreateTypeError(nodeInfo, expectedType, DeserializationResult.TypeNotInstantiable, actualType);
                    reader.MoveToEndNode();
                    return obj;
                case TypeCategory.Value:
                    return DeserializeValue(reader, expectedType, actualType);
                case TypeCategory.Collection:
                    return DeserializeCollection(reader, expectedType, actualType);
                case TypeCategory.Array:
                    return DeserializeArray(reader, expectedType, actualType);
                case TypeCategory.Object:
                default:
                    return DeserializeObject(reader, expectedType, actualType);
            }
        }

        internal bool TryMatchProperty(NodeInfoReader reader, IEnumerable<IPropertyDescriptor> properties, out IPropertyDescriptor match)
        {
            INode node = reader.Current.Node;
            foreach (IPropertyDescriptor candidate in properties)
            {
                if (!candidate.Name.Equals(node.Name, StringComparison.Ordinal))
                {
                    continue;
                }

                match = candidate;
                return true;
            }

            match = default(IPropertyDescriptor);
            return false;
        }

        private static InterfaceType GetResolver<AttributeType, InterfaceType>(Type targetType, Func<InterfaceType> defaultFactory) where AttributeType : DefaultResolverAttribute
        {
            AttributeType attribute = targetType.GetTypeInfo().GetCustomAttribute<AttributeType>();
            if (attribute != null && attribute.DefaultType != null)
            {
                try
                {
                    return attribute.CreateResolver<InterfaceType>(targetType);
                }
                catch (Exception ex)
                {
                    string typeName = attribute.DefaultType == null ? "null" : attribute.DefaultType.Name;
                    throw new BlueprintException("Error creating the class mentioned at [assembly: " + typeof(AttributeType).Name + "(typeof(" + typeName + "))]: " + ex.Message, ex);
                }
            }

            return defaultFactory();
        }

        private static bool AllowLink(Uri documentUri, Uri linkUri)
        {
            if (!linkUri.Scheme.Equals(UriExtensions.UriSchemeFile, StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }

            if (documentUri.Scheme.Equals(UriExtensions.UriSchemeFile, StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }

            if (documentUri.Scheme.Equals("memory", StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }

            return false;
        }

        private ResultType DeserializeObject(NodeInfoReader reader, Type expectedType, Type type)
        {
            NodeInfo node = reader.Current;
            IObjectDescriptor descriptor;
            IReadOnlyList<IPropertyDescriptor> required;
            IReadOnlyList<IPropertyDescriptor> optional;
            try
            { 
                descriptor = TypeResolver.GetObjectDescriptor(type);
                required = descriptor.RequiredProperties;
                optional = descriptor.OptionalProperties;
            }
            catch (BlueprintException bex)
            {
                throw new BlueprintDocumentException(bex, node.Path);
            }

            Dictionary<IPropertyDescriptor, int> remainingRequired = GetRequiredPositions(required);
            ResultType[] requiredValues = new ResultType[required.Count];
            Dictionary<IPropertyDescriptor, ResultType> optionalValues = new Dictionary<IPropertyDescriptor, ResultType>();

            IEnumerator<INode> subNodes = reader.ReadSubNodes().GetEnumerator();
            while (remainingRequired.Count > 0)
            {
                if (!subNodes.MoveNext())
                {
                    return CreateIncompleteObject(node, expectedType, descriptor, requiredValues, optionalValues);
                }

                int position;
                IPropertyDescriptor property;
                if (TryMatchProperty(reader, required, out property))
                {
                    ResultType value = Deserialize(reader, property.Type);
                    if (remainingRequired.TryGetValue(property, out position))
                    {
                        requiredValues[position] = value;
                        remainingRequired.Remove(property);
                        continue;
                    }
                }

                DeserializeProperty(reader, optional, optionalValues);
            }

            ResultType instance = CreateObject(node, expectedType, descriptor, requiredValues);
            SetDeserializedNode(node, new DeserializedNodeInfo<ResultType>(instance, type));
            while (subNodes.MoveNext())
            {
                DeserializeProperty(reader, optional, optionalValues);
            }

            SetObjectProperties(instance, optionalValues);
            return instance;
        }

        private void DeserializeProperty(NodeInfoReader reader, IReadOnlyList<IPropertyDescriptor> properties, IDictionary<IPropertyDescriptor, ResultType> values)
        {
            IPropertyDescriptor property;
            if (!TryMatchProperty(reader, properties, out property))
            {
                reader.MoveToEndNode();
                return;
            }

            ResultType value = Deserialize(reader, property.Type);
            values[property] = value;
        }

        private Dictionary<IPropertyDescriptor, int> GetRequiredPositions(IEnumerable<IPropertyDescriptor> requiredProperties)
        {
            Dictionary<IPropertyDescriptor, int> positions = new Dictionary<IPropertyDescriptor, int>();
            int position = 0;
            foreach (IPropertyDescriptor requiredProperty in requiredProperties)
            {
                positions.Add(requiredProperty, position++);
            }

            return positions;
        }

        private ResultType DeserializeCollection(NodeInfoReader reader, Type expectedType, Type type)
        {
            NodeInfo node = reader.Current;
            ICollectionDescriptor descriptor = TypeResolver.GetCollectionDescriptor(type);
            Type elementType = descriptor.ElementType;
            ResultType result = CreateCollection(node, expectedType, descriptor);
            SetDeserializedNode(node, new DeserializedNodeInfo<ResultType>(result, type));
            List<ResultType> values = new List<ResultType>();
            foreach (INode element in reader.ReadSubNodes())
            {
                values.Add(Deserialize(reader, elementType));
            }

            SetCollectionElements(result, descriptor, values);
            return result;
        }

        private ResultType DeserializeArray(NodeInfoReader reader, Type expectedType, Type type)
        {
            NodeInfo node = reader.Current;
            IArrayDescriptor descriptor = TypeResolver.GetArrayDescriptor(type);
            Type elementType = descriptor.ElementType;
            List<ResultType> values = new List<ResultType>();
            foreach (INode element in reader.ReadSubNodes())
            {
                values.Add(Deserialize(reader, elementType));
            }

            ResultType result = CreateArray(node, expectedType, descriptor, values);
            SetDeserializedNode(node, new DeserializedNodeInfo<ResultType>(result, type));
            return result;
        }

        private ResultType DeserializeValue(NodeInfoReader reader, Type expectedType, Type type)
        {
            NodeInfo node = reader.Current;
            IValueDescriptor descriptor;
            try
            {
                descriptor = TypeResolver.GetValueDescriptor(type);
            }
            catch (BlueprintException bex)
            {
                throw new BlueprintDocumentException(bex, node.Path);
            }

            ResultType result;
            string content = reader.ReadContent();
            if (descriptor.CanConvertFromString(content))
            {
                result = CreateValue(node, expectedType, descriptor, content);
            }
            else
            {
                result = CreateInvalidValue(node, expectedType, descriptor, content);
            }

            SetDeserializedNode(node, new DeserializedNodeInfo<ResultType>(result, type));
            return result;
        }

        private ResultType DeserializeLink(NodeInfo nodeInfo, Type expectedType)
        {
            if (nodeInfo.Node.Link.Equals("null"))
            {
                // Only reference types can be null
                if (expectedType.GetTypeInfo().IsValueType)
                {
                    return CreateLinkError(nodeInfo, expectedType, DeserializationResult.LinkNotCompatible);
                }

                return CreateNull(nodeInfo, expectedType);
            }

            // Links from a non-"file://" uri to a "file://" uri are not allowed
            Uri target = new Uri(nodeInfo.DocumentUri, nodeInfo.Node.Link);
            if (!AllowLink(nodeInfo.DocumentUri, target))
            {
                return CreateLinkError(nodeInfo, expectedType, DeserializationResult.LinkNotAllowed);
            }

            // The linked node may already have been deserialized
            ResultType result;
            DeserializedNodeInfo<ResultType> earlierResult;
            if (TryGetDeserializedNode(target, out earlierResult))
            {
                if (!expectedType.GetTypeInfo().IsAssignableFrom(earlierResult.TypeInfo))
                {
                    return CreateLinkError(nodeInfo, expectedType, DeserializationResult.LinkNotCompatible);
                }

                result = earlierResult.Object;
                return CreateLink(nodeInfo, expectedType, result);
            }

            // Deserialize the link
            try
            {
                result = Deserialize(target, expectedType);
                return CreateLink(nodeInfo, expectedType, result);
            }
            catch
            {
                return CreateLinkError(nodeInfo, expectedType, DeserializationResult.LinkError);
            }
        }

        private DeserializationResult GetActualType(INode node, Type expectedType, out Type actualType)
        {
            if (string.IsNullOrEmpty(node.Type))
            {
                actualType = expectedType;
            }
            else
            {
                if (!TypeResolver.TryGetType(node.Type, out actualType))
                {
                    actualType = expectedType;
                    return DeserializationResult.TypeUnknown;
                }

                if (!expectedType.GetTypeInfo().IsAssignableFrom(actualType.GetTypeInfo()))
                {
                    return DeserializationResult.TypeNotCompatible;
                }
            }

            return DeserializationResult.Success;
        }

        private bool TryGetDeserializedNode(NodeInfo nodeInfo, out DeserializedNodeInfo<ResultType> result)
        {
            if (string.IsNullOrEmpty(nodeInfo.Node.Id))
            {
                result = default(DeserializedNodeInfo<ResultType>);
                return false;
            }

            Uri uri = new Uri(nodeInfo.DocumentUri, "#" + nodeInfo.Node.Id);
            if (TryGetDeserializedNode(uri, out result))
            {
                return true;
            }

            result = default(DeserializedNodeInfo<ResultType>);
            return false;
        }

        private void SetDeserializedNode(NodeInfo nodeInfo, DeserializedNodeInfo<ResultType> result)
        {
            if (string.IsNullOrEmpty(nodeInfo.Node.Id))
            {
                return;
            }

            Uri uri = new Uri(nodeInfo.DocumentUri, "#" + nodeInfo.Node.Id);
            nodes[uri.ToString()] = result;
        }

        private bool TryGetDeserializedNode(Uri uri, out DeserializedNodeInfo<ResultType> result)
        {
            return nodes.TryGetValue(uri.ToString(), out result);
        }
    }
}